﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using HQS.TokenInterpolation.Core;
using HQS.TokenInterpolation.Tokens;

namespace HQS.TokenInterpolation
{
    public class DataSourcesVisitor : IDataSourcesVisitor
    {
        private const BindingFlags instanceFlags =
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod |
            BindingFlags.IgnoreCase;

        private readonly object _rootObject;

        private readonly Dictionary<string, object> _runtime =
            new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

        private readonly Dictionary<string, TokenRuntimeValue> _runtimeCache =
            new Dictionary<string, TokenRuntimeValue>(StringComparer.OrdinalIgnoreCase);

        public DataSourcesVisitor()
        {
        }

        /// <summary>
        ///     Creates an Instance of ParserContext
        /// </summary>
        /// <param name="rootObject">This is an object that would be used to parse any token without a handle</param>
        public DataSourcesVisitor(object rootObject)
            : this()
        {
            _rootObject = rootObject;
        }

        public object this[string key]
        {
            get
            {
                if (key == null)
                {
                    throw new ArgumentNullException("key");
                }

                object data;
                _runtime.TryGetValue(key, out data);
                return data;
            }

            set
            {
                if (key == null)
                {
                    throw new ArgumentNullException("key");
                }

                _runtime[key] = value;
            }
        }

        public void AddOrUpdate(string propertyName, object value)
        {
            this[propertyName] = value;
        }

        private IEnumerable ParseAsIEnumerable(string @this, ref string message)
        {
            IEnumerable enumerable = null;
            Result parsedResult = Parser.Parse(@this, this);
            if (parsedResult.HasErrors == false && parsedResult.TokenAndValues.Any())
            {
                enumerable = parsedResult.TokenAndValues.First().Value.RawValue as IEnumerable;
            }
            else
            {
                message = string.Join(",", parsedResult.ErrorMessages.ToArray());
            }

            return enumerable;
        }

        private TokenRuntimeValue GetRootObjectThatResolveToInstance(object obj, string handle)
        {
            var step = new TokenRuntimeValue();
            var sb = new StringBuilder("Started As:\"");
            sb.Append(handle);
            sb.Append("\" ");
            sb.Append("IResolveToken (Cast:");
            var parser = obj as IResolveToken;
            if (parser == null)
            {
                sb.Append("False");
                var dictionary = obj as IDictionary;
                sb.Append(")->IDictionary (Cast:");
                if (dictionary == null)
                {
                    sb.Append("False");

                    PropertyInfo property = obj.GetType()
                        .GetProperties(instanceFlags)
                        .SingleOrDefault(f => f.Name.Equals(handle));

                    sb.Append(")->Property (Cast:");
                    if (property != null)
                    {
                        sb.Append("True IsNull:");
                        step.RawValue = property.GetValue(obj, null);
                        step.Resolved = true;
                        sb.Append(step.RawValue == null ? "True)" : "False)");
                    }
                    else
                    {
                        sb.Append("False");
                        sb.Append(")->Field (Cast:");
                        FieldInfo field = obj.GetType()
                            .GetFields(instanceFlags)
                            .SingleOrDefault(f => f.Name.Equals(handle));

                        if (field == null)
                        {
                            sb.Append("False)");
                            step.ErrorMessage = string.Format("Handle \"{0}\" was not found in the data sources.",
                                handle);
                        }
                        else
                        {
                            sb.Append("True IsNull:");
                            step.RawValue = field.GetValue(obj);
                            step.Resolved = true;
                            sb.Append(step.RawValue == null ? "True)" : "False)");
                        }
                    }
                }
                else
                {
                    sb.Append("True IsNull:");
                    step.Resolved = dictionary.Contains(handle);
                    if (step.Resolved)
                    {
                        step.RawValue = dictionary[handle];
                    }

                    sb.Append(step.RawValue == null ? "True)" : "False)");
                }
            }
            else
            {
                sb.Append("True IsNull:");
                step = parser.Resolve(handle);
                sb.Append(step.RawValue == null ? "True)" : "False)");
            }

            step.DiagnosticMessage = sb.ToString();
            Debug.WriteLine(sb.ToString());

            if (step.Resolved && step.RawValue == null)
            {
                step.WarningMessage = string.Format("Resolving {0} result in a null instance", handle);
            }

            if (step.RawValue == null && step.Resolved == false)
            {
                step.ErrorMessage = sb.ToString();
            }

            return step;
        }

        private TokenRuntimeValue Resolve(string[] properties)
        {
            var step = new TokenRuntimeValue();
            object currentObject;
            int startIndex = 0;
            if (_runtime.TryGetValue(properties[0], out currentObject))
            {
                startIndex = 1;
            }
            else
            {
                currentObject = _rootObject;
            }

            if (currentObject != null)
            {
                for (int i = startIndex; i < properties.Length; i++)
                {
                    step = GetRootObjectThatResolveToInstance(currentObject, properties[i]);
                    currentObject = step.RawValue;
                    if (step.RawValue == null)
                    {
                        step.ErrorMessage =
                            string.Format(
                                "The parser stopped execution {0} at {1} since It yielded to a null value.",
                                (i != properties.Length - 1 ? "early" : string.Empty),
                                string.Join(".", properties.Take(i + 1).ToArray()));

                        break;
                    }
                }
            }

            return step;
        }

        private bool TryFormat(object rawValue, string format, out string value, out string errorMessage)
        {
            bool result = false;
            value = Convert.ToString(rawValue);
            errorMessage = null;
            if (rawValue != null)
            {
                if (string.IsNullOrWhiteSpace(format) == false)
                {
                    Result parserResult = Parser.Parse(format, this);
                    if (parserResult.HasErrors == false)
                    {
                        if (string.IsNullOrWhiteSpace(parserResult.Output) == false)
                        {
                            try
                            {
                                value = rawValue.GetType()
                                    .InvokeMember("ToString", instanceFlags, null, rawValue,
                                        new object[] {parserResult.Output, null})
                                    as string;
                                result = true;
                            }
                            catch (MissingMethodException)
                            {
                                errorMessage = string.Format(
                                    "Invalid Format \"{0}\" The following type \"{1}\" doesn't support IFormattable",
                                    parserResult.Output, rawValue.GetType().FullName);
                            }
                        }
                        else
                        {
                            try
                            {
                                value =
                                    rawValue.GetType().InvokeMember("ToString", instanceFlags, null, rawValue, null) as
                                        string;
                                result = true;
                            }
                            catch (MissingMethodException mme)
                            {
                                errorMessage = string.Format("The following type\"{0}\" threw an exception \"{1}\"",
                                    rawValue.GetType().FullName, mme.Message);
                            }
                        }
                    }
                    else
                    {
                        result = false;
                        errorMessage = string.Join(",", parserResult.ErrorMessages.ToArray());
                    }
                }
            }


            return result;
        }

        #region Visit Operations

        public TokenRuntimeValue Visit(Floor token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                double value;
                if (double.TryParse(result.Value, out value) == false)
                {
                    result.ErrorMessage =
                        string.Format("The following property \"{0}\" didn't resolve as a numerical value \"{1}\".",
                            token.This, result.Value);
                    result.Value = null;
                    result.RawValue = null;
                }
                else
                {
                    result.Value = Math.Floor(value).ToString();
                    string strValue;
                    string warningMessage;
                    TryFormat(result.Value, token.Format, out strValue, out warningMessage);
                    result.WarningMessage = warningMessage;
                    result.Value = strValue;
                    result.RawValue = value;
                }
            }

            return result;
        }

        public TokenRuntimeValue Visit(Ceiling token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                double value;
                if (double.TryParse(result.Value, out value) == false)
                {
                    result.ErrorMessage =
                        string.Format("The following property \"{0}\" didn't resolve as a numerical value \"{1}\".",
                            token.This, result.Value);
                    result.Value = null;
                    result.RawValue = null;
                }
                else
                {
                    result.Value = Math.Ceiling(value).ToString();
                    string strValue;
                    string warningMessage;
                    TryFormat(result.Value, token.Format, out strValue, out warningMessage);
                    result.WarningMessage = warningMessage;
                    result.Value = strValue;
                    result.RawValue = value;
                }
            }

            return result;
        }

        public TokenRuntimeValue Visit(If token)
        {
            TokenRuntimeValue result = Parse(token.Expression);
            if (result.Value != null)
            {
                ParserResult<bool?> expressionResult = LogicalExpressionParser.Parse(result.Value);
                if (expressionResult.HasErrors)
                {
                    result.ErrorMessage = string.Join(",", expressionResult.ErrorMessages.ToArray());
                    result.Value = null;
                    result.RawValue = null;
                }
                else
                {
                    if (expressionResult.Output.HasValue && expressionResult.Output.Value)
                    {
                        result = Parse(token.Then);
                    }
                    else
                    {
                        result = Parse(token.Else);
                    }
                }
            }

            return result;
        }

        public TokenRuntimeValue Visit(Join token)
        {
            // Needs revisiting 
            string errorMessage = null;
            IEnumerable enumerable = ParseAsIEnumerable(token.This, ref errorMessage);

            var data = new List<string>();
            if (enumerable != null)
            {
                IEnumerator enumerator = enumerable.GetEnumerator();
                while (enumerator.MoveNext())
                {
                    string handle = token.This.Replace('}', ' ').Replace('{', ' ').Trim();
                    string newEnumerableToken = handle + "_Current";
                    string oldEnumerableToken = handle + ".Current";

                    _runtimeCache[newEnumerableToken] = new TokenRuntimeValue
                    {
                        RawValue = enumerator.Current,
                        Resolved = true
                    };

                    _runtime[newEnumerableToken] = enumerator.Current;

                    Result result = Parser.Parse(token.Using.Replace(oldEnumerableToken, newEnumerableToken), this);
                    if (result.HasErrors == false)
                    {
                        data.Add(result.Output);
                    }

                    _runtimeCache.Keys.Where(f => f.StartsWith(handle)).ToList().ForEach(f => _runtimeCache.Remove(f));
                    _runtime.Keys.Where(f => f.StartsWith(handle)).ToList().ForEach(f => _runtimeCache.Remove(f));
                }
            }

            return new TokenRuntimeValue
            {
                RawValue = string.Join(token.By, data.ToArray()),
                Value = string.Join(token.By, data.ToArray()),
                Resolved = enumerable != null,
                ErrorMessage = enumerable != null ? null : token.This + " is not an enumerable instance"
            };
        }

        public TokenRuntimeValue Visit(Maximum token)
        {
            return Visit((Sum) token);
        }

        public TokenRuntimeValue Visit(Minimum token)
        {
            return Visit((Sum) token);
        }

        public TokenRuntimeValue Visit(Average token)
        {
            return Visit((Sum) token);
        }

        public TokenRuntimeValue Visit(Sum token)
        {
            string errorMessage = null;
            IEnumerable enumerable = ParseAsIEnumerable(token.This, ref errorMessage);
            var values = new List<double>();
            if (enumerable != null)
            {
                IEnumerator enumerator = enumerable.GetEnumerator();
                while (enumerator.MoveNext())
                {
                    string handle = token.This.Replace('}', ' ').Replace('{', ' ').Trim();


                    string newEnumerableToken = handle.Replace('.', '_') + "_Current";


                    string oldEnumerableToken = handle + ".Current";

                    _runtimeCache[newEnumerableToken] = new TokenRuntimeValue
                    {
                        RawValue = enumerator.Current,
                        Resolved = true
                    };

                    _runtime[newEnumerableToken] = enumerator.Current;

                    TokenRuntimeValue result = Parse(token.Expression.Replace(oldEnumerableToken, newEnumerableToken));
                    if (result.Value != null)
                    {
                        ParserResult<double?> parserResult = MathParser.Parse(result.Value);
                        if (parserResult.HasErrors == false)
                        {
                            values.Add(parserResult.Output.Value);
                        }
                    }

                    _runtimeCache.Keys.Where(f => f.Contains(newEnumerableToken))
                        .ToList()
                        .ForEach(f => _runtimeCache.Remove(f));
                    _runtime.Keys.Where(f => f.Contains(newEnumerableToken))
                        .ToList()
                        .ForEach(f => _runtimeCache.Remove(f));
                }

                double value = token.Aggregate(values);
                string strValue;
                string warningMessage;
                TryFormat(value, token.Format, out strValue, out warningMessage);
                return new TokenRuntimeValue
                {
                    RawValue = value,
                    Resolved = true,
                    Value = strValue,
                    WarningMessage = warningMessage
                };
            }

            return new TokenRuntimeValue {RawValue = "", Resolved = true, Value = "", ErrorMessage = errorMessage};
        }

        public TokenRuntimeValue Visit(Compute token)
        {
            TokenRuntimeValue result = Parse(token.Expression);
            if (result.Value != null)
            {
                try
                {
                    ParserResult<double?> parserResult = MathParser.Parse(result.Value);
                    if (parserResult.HasErrors == false)
                    {
                        string value;
                        string warningMessage;
                        TryFormat(parserResult.Output.Value, token.Format, out value, out warningMessage);
                        result.WarningMessage = warningMessage;
                        result.Value = value;
                        result.RawValue = parserResult.Output;
                    }
                }
                catch (Exception e)
                {
                    result.ErrorMessage = e.Message;
                }
            }

            return result;
        }

        public TokenRuntimeValue Visit(ForEach token)
        {
            // Needs revisiting 
            string errorMessage = null;
            IEnumerable enumerable = ParseAsIEnumerable(token.This, ref errorMessage);

            var sb = new StringBuilder();
            if (enumerable != null)
            {
                IEnumerator enumerator = enumerable.GetEnumerator();
                int position = 1;
                while (enumerator.MoveNext())
                {
                    // Hack : TODO Rework it. 
                    string handle = string.Format("{0}_Current", token.TemplateName);
                    _runtimeCache[handle] = new TokenRuntimeValue
                    {
                        RawValue = enumerator.Current,
                        Resolved = true
                    };
                    _runtime[handle] = enumerator.Current;
                    _runtimeCache[token.TemplateName + ".Position"] = new TokenRuntimeValue
                    {
                        Resolved = true,
                        RawValue = position,
                        Value = position.ToString()
                    };

                    Result result = Parser.Parse(
                        token.Template.Content.Replace(token.TemplateName + ".Current", handle),
                        this);
                    sb.Append(result.Output);


                    _runtimeCache.Keys.Where(f => f.StartsWith(handle)).ToList().ForEach(f => _runtimeCache.Remove(f));
                    _runtime.Keys.Where(f => f.StartsWith(handle)).ToList().ForEach(f => _runtimeCache.Remove(f));
                    _runtimeCache.Remove(token.TemplateName + ".Position");
                    _runtime.Remove(token.TemplateName + ".Position");
                    position++;
                }
            }

            return new TokenRuntimeValue
            {
                RawValue = sb.ToString(),
                Value = sb.ToString(),
                Resolved = enumerable != null,
                ErrorMessage = enumerable != null ? null : token.This + " is not an enumerable instance"
            };
        }

        public TokenRuntimeValue Visit(DefaultToken token)
        {
            TokenRuntimeValue result = null;
            TokenRuntimeValue resolvedTokenRuntimeValue = null;
            string namedParameter =
                token.Name.Replace(Scanner.LeftCurlyBracket, ' ').Replace(Scanner.RightCulryBracket, ' ').Trim();

            if (_runtimeCache.TryGetValue(namedParameter, out resolvedTokenRuntimeValue) == false)
            {
                string[] properties = namedParameter.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries);
                _runtimeCache.Add(namedParameter, Resolve(properties));
            }


            result = _runtimeCache[namedParameter].Clone() as TokenRuntimeValue;
            if (result.RawValue != null)
            {
                string value;
                string warningMessage;
                TryFormat(result.RawValue, token.Format, out value, out warningMessage);
                result.Value = value;
                result.WarningMessage = warningMessage;
            }

            return result;
        }

        public TokenRuntimeValue Visit(SubString token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                result.Value = result.Value.Substring(token.From,
                    (token.Length > 0 && token.Length <= result.Value.Length) ? token.Length : result.Value.Length);
                result.RawValue = result.Value;
            }

            return result;
        }

        public TokenRuntimeValue Visit(Repeat token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                result.RawValue = Enumerable.Repeat(result.Value, token.NumberOfTimes);
            }

            return result;
        }

        public TokenRuntimeValue Visit(Replace token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                string target = result.Value;
                result = Parse(token.With);
                string with = result.Value;
                if (result.Value != null)
                {
                    result = Parse(token.That);
                    if (result.Value != null)
                    {
                        result.Value = token.IgnoreCase
                            ? StringUtil.CaseInsenstiveReplace(target, with, result.Value)
                            : target.Replace(with, result.Value);

                        result.RawValue = result.Value;
                    }
                }
            }


            return result;
        }

        public TokenRuntimeValue Visit(Coalesce token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (string.IsNullOrWhiteSpace(result.Value))
            {
                var withList = new List<string>();
                Action<string> addIfNotNullOrEmpty = (string f) =>
                {
                    if (string.IsNullOrWhiteSpace(f) == false)
                    {
                        withList.Add(f);
                    }
                };

                addIfNotNullOrEmpty(token.With);
                addIfNotNullOrEmpty(token.With1);
                addIfNotNullOrEmpty(token.With2);
                addIfNotNullOrEmpty(token.With3);

                foreach (string with in withList)
                {
                    result = Parse(with);
                    if (string.IsNullOrWhiteSpace(result.Value) == false)
                    {
                        break;
                    }
                }

                if (result.Resolved == false)
                {
                    result.ErrorMessage = "All with[s] properties evaluated null or empty";
                }
            }

            return result;
        }

        public TokenRuntimeValue Visit(Right token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                if (result.Value.Length > token.Length)
                {
                    result.Value = result.Value.Substring(result.Value.Length - token.Length, token.Length);
                    result.RawValue = result.Value;
                }
            }

            return result;
        }

        public TokenRuntimeValue Visit(Left token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                result.Value = result.Value.Substring(0, Math.Min(token.Length, result.Value.Length));
                result.RawValue = result.Value;
            }

            return result;
        }

        public TokenRuntimeValue Visit(Split token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                string target = result.Value;
                result = Parse(token.By);
                if (result.Value != null)
                {
                    result.RawValue = token.RemoveEmpty
                        ? target.Split(result.Value.ToCharArray(), StringSplitOptions.RemoveEmptyEntries)
                        : target.Split(result.Value.ToCharArray(), StringSplitOptions.None);
                    result.Value = target;
                }
            }

            return result;
        }

        public TokenRuntimeValue Visit(PadRight token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                int len;
                TokenRuntimeValue resultLen = Parse(token.Length);
                if (resultLen.Value != null && int.TryParse(resultLen.Value, out len) && len > 0)
                {
                    result.Value = result.Value.PadRight(len, token.With == '\0' ? ' ' : token.With);
                    result.RawValue = result.Value;
                }
            }

            return result;
        }

        public TokenRuntimeValue Visit(PadLeft token)
        {
            TokenRuntimeValue result = Parse(token.This);
            if (result.Value != null)
            {
                int len;
                TokenRuntimeValue resultLen = Parse(token.Length);
                if (resultLen.Value != null && int.TryParse(resultLen.Value, out len) && len > 0)
                {
                    result.Value = result.Value.PadLeft(len, token.With == '\0' ? ' ' : token.With);
                    result.RawValue = result.Value;
                }
            }

            return result;
        }

        private TokenRuntimeValue Parse(string data)
        {
            var runtime = new TokenRuntimeValue();
            runtime.Resolved = true;
            Result result = Parser.Parse(data, this);
            if (result.HasErrors == false)
            {
                runtime.Value = result.Output;
                runtime.RawValue = result.Output;
            }
            else
            {
                runtime.ErrorMessage = string.Join(",", result.ErrorMessages.ToArray());
            }

            return runtime;
        }

        #endregion
    }
}